import buffer from '@ohos.buffer';
import socket from '@ohos.net.socket';
import { EventEmitter } from "events";
import CryptoJS from '@ohos/crypto-js';
import Session, { AuthenticateOptions } from "./Session";
import { Packet, Smb2Header, Smb2Request, Smb2Response, Smb2StatusCode,
  Smb2PacketType, structureUtil } from "@dove/samba_protocol";

export interface Options {
  port?: number;
  connectTimeout?: number;
  requestTimeout?: number;
}

interface Client {
  on(event: "error", callback: (error: Error) => void): this;

  on(event: "changeNotify", callback: (response: Smb2Response) => void): this;

  once(event: "error", callback: (error: Error) => void): this;

  once(event: "changeNotify", callback: (response: Smb2Response) => void): this;
}

class Client extends EventEmitter {
  _id = CryptoJS.lib.WordArray.random(4).toString(CryptoJS.enc.Hex);
  socket: socket.TCPSocket;
  nextMessageId: bigint = BigInt(0);
  responseRestChunk: buffer.Buffer;
  responseMap = new Map<bigint, Smb2Response>();
  responseCallbackMap = new Map<bigint, (response: Smb2Response) => void>();
  connected: boolean = false;
  port: number = 445;
  connectTimeout: number = 5 * 1000;
  connectTimeoutId: number;
  requestTimeout: number = 5 * 1000;
  requestTimeoutIdMap = new Map<bigint, number>();
  sessions: Session[] = [];

  constructor(public host: string, public options: Options = {}) {
    super();
    if (typeof this.options.port === "number") this.port = this.options.port;
    if (typeof this.options.connectTimeout === "number") this.connectTimeout = this.options.connectTimeout;
    if (typeof this.options.requestTimeout === "number") this.requestTimeout = this.options.requestTimeout;
  }

  async connect() {
    if (this.connected) return;
    this.socket = socket.constructTCPSocketInstance();
    this.socket.on("message", this.onData);
    this.socket.setExtraOptions({ keepAlive: true });
    this.socket.on("error", this.onError)
    this.socket.on("close", this.onClose);
    const connectPromise = new Promise<void>((resolve, reject) => {
      this.connectTimeoutId = setTimeout(() => {
        reject(new Error("connect_timeout"));
      }, this.connectTimeout);
      this.socket.on("connect", () => {
        resolve();
      });
      this.socket.on("error", (err) => {
        reject(err);
      }); //?
      this.socket.connect({
        timeout: 0,
        address: { port: this.port, address: this.host }
      });
    });
    try {
      await connectPromise;
      clearTimeout(this.connectTimeoutId);
      this.connected = true;
    } catch (err) {
      this.destroySocket();
      throw err;
    }
  }

  createRequest(header: Smb2Header = {}, body: any = {}) {
    const messageId = this.nextMessageId++;
    return new Smb2Request({ messageId, clientId: this._id, ...header }, body);
  }

  async request(header?: Smb2Header, body?: any) {
    const request = this.createRequest(header, body);
    return await this.send(request);
  }

  async send(request: Smb2Request) {
    if (!this.connected) throw new Error("not_connected");
    const buffer = request.serialize();
    this.socket.send({ data: buffer.buffer });
    const messageId = request.header.messageId;
    const sendPromise = new Promise<Smb2Response>((resolve, reject) => {
      const requestTimeoutId = setTimeout(() => {
        const err = new Error(`request_timeout: ${structureUtil.parseEnumValue(Smb2PacketType, request.header.type)}(${messageId})`);
        reject(err);
      }, this.requestTimeout);
      this.requestTimeoutIdMap.set(messageId, requestTimeoutId);
      const finishRequest = (response: Smb2Response) => {
        response.request = request;
        if (response.header.status !== Smb2StatusCode.Success && response.header.status !== Smb2StatusCode.Pending && response.header.status !== Smb2StatusCode.MoreProcessingRequired && response.header.status !== Smb2StatusCode.FileClosed) {
          reject(response);
        } else
          resolve(response);
      };
      if (this.responseMap.has(messageId)) {
        finishRequest(this.responseMap.get(messageId));
        this.responseMap.delete(messageId);
      } else if (!this.responseCallbackMap.has(messageId))
        this.responseCallbackMap.set(messageId, finishRequest);
    });
    const response = await sendPromise;
    if (this.requestTimeoutIdMap.has(messageId)) {
      const requestTimeoutId = this.requestTimeoutIdMap.get(messageId);
      clearTimeout(requestTimeoutId);
      this.requestTimeoutIdMap.delete(messageId);
    }
    return response;
  }

  onData = (info: socket.SocketMessageInfo) => {
    let buf;
    if (this.responseRestChunk) {
      const b = buffer.from(info.message);
      buf = buffer.concat([this.responseRestChunk, b]);
      this.responseRestChunk = undefined;
    }
    const {chunks,restChunk} = Packet.getChunks(buf);
    this.responseRestChunk = restChunk;
    for (const chunk of chunks) {
      const response = Smb2Response.parse(chunk);
      this.onResponse(response);
    }
  }

  onResponse(response: Smb2Response) {
    if (response.header.type === Smb2PacketType.ChangeNotify && response.header.status === Smb2StatusCode.Success)
      this.emit("changeNotify", response);
    const messageId = response.header.messageId;
    if (this.responseCallbackMap.has(messageId)) {
      this.responseCallbackMap.get(messageId)(response);
      this.responseCallbackMap.delete(messageId);
    } else
      this.responseMap.set(messageId, response);
  }

  onError = (err: Error) => {
    this.emit("error", err);
  }

  onClose = () => {
    this.connected = false;
    this.emit("error", new Error("client_closed"));
  }

  async echo() {
    return await this.request({
      type: Smb2PacketType.Echo
    });
  }

  async authenticate(options: AuthenticateOptions) {
    if (!this.connected) await this.connect();
    const session = new Session(this);
    this.registerSession(session);
    await session.authenticate(options);
    return session;
  }

  private destroySocket() {
    this.socket.off("message", this.onData);
    this.socket.off("error", this.onError);
    this.socket.off("close", this.onClose);
    this.socket.close();
    delete this.socket;
  }

  private registerSession(session: Session) {
    session.once("authenticate", () => this.sessions.push(session)).once("logoff", () => this.sessions.splice(this.sessions.indexOf(session), 1));
  }

  async close() {
    if (!this.connected) return;
    await Promise.all(this.sessions.map(x => x.logoff()));
    this.destroySocket();
    this.connected = false;
  }
}
export default Client;